Состав портфолио¶

In [ ]:
%load_ext autoreload
%autoreload 2
The autoreload extension is already loaded. To reload it, use:
  %reload_ext autoreload

Historical data¶

Data have been taken from http://www.lazyportfolioetf.com .

Stocks¶

  • Russell 2000 - American Small-Cap market ETF
  • VSS - FTSE All-World ex-US Small-Cap ETF
  • EFA - ex-US Large-Cap market ETF tracking EAFE
  • VTI - CRSP US Total Market Index
  • VNQ - US REIT ETF
  • VUG - CRSP US Large Cap Growth Index ETF
  • GLD - Gold
  • DBC - Commodities

Bonds¶

  • BND - Vanguard Total Bond Market
  • SHY - iShares 1-3 Year Treasury Bond

Analytics¶

In [ ]:
from collections import OrderedDict
from src.domain.share_type import StockInfo, BondInfo, Term, Region, Cap
from src.statistics import get_max_first_year
from src.laxy_portfolio_etf import get_returns_data

stocks_data = OrderedDict({
    "VTI": get_returns_data("data/monthly-returns/vti-returns-monthly.txt"),
    "EAFE": get_returns_data("data/monthly-returns/eafe-returns-monthly.txt"),
    "VUG": get_returns_data("data/monthly-returns/vug-returns.txt"),
    "Russell 2000": get_returns_data("data/monthly-returns/russell_2000-returns-monthly.txt"),
    "EAFE Small-Cap": get_returns_data("data/monthly-returns/eafe-small-cap-return-monthly.txt"),
    "VNQ (REIT)": get_returns_data("data/monthly-returns/vnq-returns.txt"),
    "GLD": get_returns_data("data/monthly-returns/gld-returns.txt"),
    "DBC": get_returns_data("data/monthly-returns/dbc-returns.txt"),
    "BND": get_returns_data("data/monthly-returns/bnd-returns.txt"),
    "SHY": get_returns_data("data/monthly-returns/shy-returns.txt"),
})

shares_info = OrderedDict({
    "VTI": StockInfo(region=Region.US, cap=Cap.Large),
    "VUG": StockInfo( region=Region.US, cap=Cap.Large),
    "VNQ (REIT)": StockInfo(region=Region.US, cap=Cap.Large),
    "EAFE": StockInfo( region=Region.ExUS, cap=Cap.Large),
    "Russell 2000": StockInfo(region=Region.US, cap=Cap.Small),
    "EAFE Small-Cap": StockInfo(region=Region.ExUS, cap=Cap.Small),
    "GLD": StockInfo(region=Region.US, cap=Cap.Large),
    "DBC": StockInfo(region=Region.US, cap=Cap.Large),
    "BND": BondInfo(region=Region.US, term=Term.Long),
    "SHY": BondInfo(region=Region.US, term=Term.Short),
})

max_first_year = get_max_first_year(stocks_data.values())
print("max_first_year", max_first_year)
max_first_year 1992

Monthly returns plot¶

In [ ]:
import numpy as np
import pandas as pd
import plotly
import plotly.express as px
from src.statistics import get_max_returns_from_same_year, get_max_first_year, calc_returns_amount_list
from src.plot_utils import get_monthly_data_range

plotly.offline.init_notebook_mode()

max_first_year = get_max_first_year(stocks_data.values())
init_amount = 1000
data = [calc_returns_amount_list(init_amount, x) for x in get_max_returns_from_same_year(stocks_data.values())]

df = pd.DataFrame(
    np.array(data).transpose(),
    columns=list(stocks_data.keys()), 
    index=get_monthly_data_range(max_first_year, 2021)
)

fig = px.line(df, x=df.index, y=df.columns)
fig.show(renderer="notebook+pdf")

Correlations¶

In [ ]:
import numpy as np
from src.statistics import get_correlation_table_view, get_stocks_std_view, calc_annual_geometric_mean
    
stock_names = list(stocks_data.keys())
corr_table = get_correlation_table_view(stocks_data)

index = stock_names
columns = ["From year", "Mean ret.", "Std", "|", *stock_names]

splitter_column = ["|"] * len(index)
from_column = [x.first_year for x in stocks_data.values()]
annual_mean_column = [f'{round(calc_annual_geometric_mean(x), 2)}%' for x in stocks_data.values()]
std_column = get_stocks_std_view(stocks_data)

df = pd.DataFrame(
    np.array([from_column, annual_mean_column, std_column, splitter_column, *corr_table]).transpose(),
    columns=columns,
    index=index
)
df
Out[ ]:
From year Mean ret. Std | VTI EAFE VUG Russell 2000 EAFE Small-Cap VNQ (REIT) GLD DBC BND SHY
VTI 1972 10.98% 15.55% | - 70.62% 96.05% 89.34% 73.6% 61.69% 1.88% 17.08% 16.64% 4.38%
EAFE 1986 7.08% 17.4% | 70.62% - 65.17% 62.51% 85.57% 50.36% 12.83% 32.65% 9.71% -8.3%
VUG 1972 11.1% 16.8% | 96.05% 65.17% - 80.92% 66.28% 52.26% 0.84% 12.25% 17.49% 5.74%
Russell 2000 1985 10.13% 19.74% | 89.34% 62.51% 80.92% - 71.96% 62.29% -3.13% 24.19% 0.76% -19.11%
EAFE Small-Cap 1992 7.95% 16.47% | 73.6% 85.57% 66.28% 71.96% - 57.33% 18.85% 47.89% 5.91% -21.97%
VNQ (REIT) 1972 11.48% 17.09% | 61.69% 50.36% 52.26% 62.29% 57.33% - 5.77% 14.96% 18.19% 3.73%
GLD 1972 7.6% 19.9% | 1.88% 12.83% 0.84% -3.13% 18.85% 5.77% - 27.34% 7.79% 7.08%
DBC 1971 8.21% 18.7% | 17.08% 32.65% 12.25% 24.19% 47.89% 14.96% 27.34% - -3.46% -1.32%
BND 1972 6.72% 5.3% | 16.64% 9.71% 17.49% 0.76% 5.91% 18.19% 7.79% -3.46% - 87.64%
SHY 1977 5.54% 3.03% | 4.38% -8.3% 5.74% -19.11% -21.97% 3.73% 7.08% -1.32% 87.64% -

My portfolio¶

In [ ]:
from src.domain.distribution import SharesDistribution
from src.domain.share_type import Cap, Term, Region, ShareType

TOTAL = 1

print(dict(zip((Term.Long, ), (1, ))))
print(Region.US.value)

distribution = SharesDistribution(
    by_type=dict(zip((ShareType.Stock, ShareType.Bond), (0.75, 0.25))),
    by_region=dict(zip((Region.US, Region.ExUS), (0.7, 0.3))),
    by_cap=dict(zip((Cap.Large, Cap.Small), (0.7, 0.3))),
    by_term=dict(zip((Term.Long, ), (1, ))),
)

large_us_stocks = {
    "VTI": 0.8,
    "VNQ (REIT)": 0.2
}

large_exus_stocks = {
    "EAFE": 1
}

small_us_stocks = {
    "Russell 2000": 1
}

small_exus_stocks = {
    "EAFE Small-Cap": 1
}

bonds = {
    "BND": 1
}

def assert_shares(obj):
    assert sum(obj.values()) == TOTAL

fund_shares = [large_us_stocks, large_exus_stocks, small_us_stocks, small_exus_stocks, bonds]

for shares in [distribution.by_cap, distribution.by_region, distribution.by_term, distribution.by_type] + fund_shares:
    assert_shares(shares)
{<Term.Long: 'long'>: 1}
US

Плоское портфолио акций¶

In [ ]:
flatten_stock_portfolio = []

stock_share = distribution.by_type[ShareType.Stock]

for (region_key, region_value) in distribution.by_region.items():
    for (cap_key, cap_value) in distribution.by_cap.items():
        flatten_stock_portfolio.append({
            "region": region_key.value,
            "share": stock_share * region_value * cap_value,
            "cap": cap_key.value
        })
        
def sort_by_share(portfolio):
    return sorted(portfolio, key=lambda x: x["share"], reverse=True)
        
flatten_stock_portfolio = sort_by_share(flatten_stock_portfolio)
In [ ]:
import pandas as pd

def share_to_percent(share):
    return f'{round(share * 100, 2)}%'

def prepare_stock_table_rows(flatten_portfolio):
    stocks_table = []
    
    full_share = 0
    for row in flatten_portfolio:
        full_share += row["share"]
    
    for stock in flatten_portfolio:
        share = stock["share"]
        stocks_table.append([
            share_to_percent(share / full_share),
            share_to_percent(share)
        ])

    return stocks_table
    
def get_stock_name(share):
    return f'{share["region"].capitalize()} {share["cap"].capitalize()}-cap stocks'

stocks_table = prepare_stock_table_rows(flatten_stock_portfolio)
    
df = pd.DataFrame(stocks_table, columns=['Share', 'Share of all'], index=[get_stock_name(x) for x in flatten_stock_portfolio])
df
Out[ ]:
Share Share of all
Us Large-cap stocks 49.0% 36.75%
Us Small-cap stocks 21.0% 15.75%
Ex-us Large-cap stocks 21.0% 15.75%
Ex-us Small-cap stocks 9.0% 6.75%
In [ ]:
import pandas as pd
from IPython.display import display
from src.portfolio import prepare_stock_table_rows, get_stock_name, get_fund_distribution, calc_portfolio_std, calc_portfolio_returns
from src.statistics import calc_annual_geometric_mean_from_monthly

stocks_table = prepare_stock_table_rows(flatten_stock_portfolio)
    
df = pd.DataFrame(stocks_table, columns=['Share', 'Share of all'], index=[get_stock_name(x) for x in flatten_stock_portfolio])
display(df)

view_fund_distrib = get_fund_distribution(fund_shares, shares_info, distribution)

percent_column = [f'{round(x * 100, 2)}%' for x in view_fund_distrib.values()]
df2 = pd.DataFrame(np.array(percent_column).transpose(), columns=['%'], index=list(view_fund_distrib.keys()))
display(df2)

portfolio_std = calc_portfolio_std(view_fund_distrib, stocks_data)

display(f'Portfolio standard deviation: {round(portfolio_std, 2)}%')

portfolio_returns, max_first_year = calc_portfolio_returns(view_fund_distrib, stocks_data)
display(f'Portfolio annual mean returns: {round(calc_annual_geometric_mean_from_monthly(portfolio_returns), 2)}%')

init_amount = 1000
portoflio_returns_amounts = calc_returns_amount_list(init_amount, portfolio_returns)

df = pd.DataFrame(
    np.array(portoflio_returns_amounts).transpose(),
    columns=["Portfolio"], 
    index=get_monthly_data_range(max_first_year, 2021)
)

fig = px.line(df, x=df.index, y=df.columns)
fig.show(renderer="notebook+pdf")
Share Share of all
Us Large-cap stocks 49.0% 36.75%
Us Small-cap stocks 21.0% 15.75%
Ex-us Large-cap stocks 21.0% 15.75%
Ex-us Small-cap stocks 9.0% 6.75%
%
VTI 29.4%
VNQ (REIT) 7.35%
EAFE 15.75%
Russell 2000 15.75%
EAFE Small-Cap 6.75%
BND 25.0%
'Portfolio standard deviation: 11.24%'
'Portfolio annual mean returns: 8.62%'

Variate portfolio by Stock/Bond ratio¶

In [ ]:
import dataclasses
from src.plot_utils import get_monthly_data_range

default_distribution = SharesDistribution(
    by_type=dict(zip((ShareType.Stock, ShareType.Bond), (0.75, 0.25))),
    by_region=dict(zip((Region.US, Region.ExUS), (0.7, 0.3))),
    by_cap=dict(zip((Cap.Large, Cap.Small), (0.7, 0.3))),
    by_term=dict(zip((Term.Long, ), (1, ))),
)

def create_distrib_by_type_ratio(stock_rate: float):
    bond_rate = 1 - stock_rate
    return dataclasses.replace(default_distribution, by_type=dict(zip((ShareType.Stock, ShareType.Bond), (stock_rate, bond_rate))))

distributions_rates = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]
distributions_names = [f'{round(x * 100)}/{round((1 - x) * 100)}' for x in distributions_rates]
distributions = [create_distrib_by_type_ratio(x) for x in distributions_rates]

distributions_by_fund = [get_fund_distribution(fund_shares, shares_info, x) for x in distributions]

percent_columns = [[f'{round(x * 100, 2)}%' for x in distrib.values()] for distrib in distributions_by_fund]
df = pd.DataFrame(np.array(percent_columns).transpose(), columns=distributions_names, index=list(distributions_by_fund[0].keys()))
display(df)

distributions_std = [f'{round(calc_portfolio_std(x, stocks_data), 2)}%' for x in distributions_by_fund]

max_first_year = 1992
distributions_returns = [calc_portfolio_returns(x, stocks_data)[0] for x in distributions_by_fund]

distributions_annual_mean = [f'{round(calc_annual_geometric_mean_from_monthly(x), 2)}%' for x in distributions_returns]

df = pd.DataFrame(np.array([distributions_annual_mean, distributions_std]).transpose(), columns=["Mean ret.", "Std"], index=distributions_names)
display(df)

init_amount = 1000
distributions_returns_amounts = [calc_returns_amount_list(init_amount, x) for x in distributions_returns]

df = pd.DataFrame(
    np.array(distributions_returns_amounts).transpose(),
    columns=distributions_names, 
    index=get_monthly_data_range(max_first_year, 2021)
)

fig = px.line(df, x=df.index, y=df.columns)
fig.show(renderer="notebook+pdf")
0/100 10/90 20/80 30/70 40/60 50/50 60/40 70/30 80/20 90/10 100/0
VTI 0.0% 3.92% 7.84% 11.76% 15.68% 19.6% 23.52% 27.44% 31.36% 35.28% 39.2%
VNQ (REIT) 0.0% 0.98% 1.96% 2.94% 3.92% 4.9% 5.88% 6.86% 7.84% 8.82% 9.8%
EAFE 0.0% 2.1% 4.2% 6.3% 8.4% 10.5% 12.6% 14.7% 16.8% 18.9% 21.0%
Russell 2000 0.0% 2.1% 4.2% 6.3% 8.4% 10.5% 12.6% 14.7% 16.8% 18.9% 21.0%
EAFE Small-Cap 0.0% 0.9% 1.8% 2.7% 3.6% 4.5% 5.4% 6.3% 7.2% 8.1% 9.0%
BND 100% 90.0% 80.0% 70.0% 60.0% 50.0% 40.0% 30.0% 20.0% 10.0% 0%
Mean ret. Std
0/100 5.12% 3.69%
10/90 5.66% 3.71%
20/80 6.17% 4.31%
30/70 6.67% 5.28%
40/60 7.14% 6.46%
50/50 7.6% 7.76%
60/40 8.02% 9.12%
70/30 8.43% 10.53%
80/20 8.81% 11.96%
90/10 9.16% 13.41%
100/0 9.49% 14.87%

TODO¶

  • Variate portfolio by region, cap size
  • Add distribution by growth/value stocks
  • Calculate correlations for Portfolio vs. S&P 500/US Treasure Bonds
  • Research current P/E, P/B by sectors

  • Move data initialization to Python scripts

  • Automate notebook HTML view updates